/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */
package cn.hermit.data.xmljson.handler;

import cn.hermit.data.json.factory.OrderedJSONFactory;
import cn.hermit.data.json.model.JSONArray;
import cn.hermit.data.json.model.JSONDocument;
import cn.hermit.data.json.model.JSONFactory;
import cn.hermit.data.json.model.JSONObject;
import cn.hermit.core.FastStack;
import cn.hermit.util.StringUtils;
import cn.hermit.data.xml.XmlChar;
import cn.hermit.data.xml.handler.ContentHandler;
import cn.hermit.data.xml.model.XmlNameNode;
import cn.hermit.data.xml.validator.ValidatorChain;
import cn.hermit.data.xmljson.XMLJSON;

import javax.xml.namespace.QName;
import java.util.Map.Entry;

/**
 * Content handler for conversion from XML to JSON. 
 * 
 * @author Jack Gao (Chinese name : GAO JIANGUO, Email : linux.gjg@gmail.com)
 * @date 27 Jun, 2014
 */
public class X2JHandler implements ContentHandler {

	/**
	 * Indicate if ignore name space definition in conversion from XML to JSON.
	 */
	protected boolean ignoredNamespace;
	/**
	 * Indicate if ignore name space prefix in conversion from XML to JSON.
	 */
	protected boolean ignoredPrefix;

	/**
	 * Indicate if need to treat element tag as the JSON key for text node in element.
	 */
	protected boolean tagAskeyforSingleText = true;

	/**
	 * Indicate if need to keep empty element.
	 */
	protected boolean keepEmptyElement;

	/**
	 * Indicate if transform tag-closed element to null JSON value.
	 */
	protected boolean closedElement2null;

	/**
	 * Indicate if need to remove JSON root before conversion.
	 */
	protected boolean removeRoot;

	/**
	 * Indicate the JSON key for XML text node.
	 */
	private String textKey = XMLJSON.DEFAULT_TEXT_JSON_KEY;

	// //////////////////////////////////////

	/**
	 * JSON factory to create object in conversion.
	 */
	protected JSONFactory jsonFactory;

	/**
	 * A stack for JSON object generating recursively.
	 */
	protected FastStack<JSONDocument> stack;

	/**
	 * The XML Validator
	 */
	private ValidatorChain validator;

	// /////////////////////////////////////

	
	/**
	 * Constructor of content handler for conversion from XML to JSON. Initialize an ordered JSON factory.
	 */
	public X2JHandler() {
		this(new OrderedJSONFactory());
	}

	/**
	 * Constructor of content handler for conversion from XML to JSON.<br/>
	 * 
	 * @param jsonFactory
	 * 		JSON factory to create JSON objects. If it is null, will initialize an ordered JSON factory.
	 */
	public X2JHandler(JSONFactory jsonFactory) {
		if (jsonFactory == null) {
			jsonFactory = new OrderedJSONFactory();
		}
		this.jsonFactory = jsonFactory;
		stack = new FastStack<JSONDocument>();
		stack.push(this.jsonFactory.createObject());
		validator = new ValidatorChain();
	}

	@Override
	public void startDocument() {

	}

	@Override
	public void version(String version) {
		// ignored
	}

	@Override
	public void encoding(String encoding) {
		// ignored
	}

	@Override
	public void standalone(String standalone) {
		// ignored
	}

	/**
	 * Start converting when this method is invoked. And convert XML Element to JSON objects recursively.
	 * @param name
	 * 		QName of the XML Element
	 * @param closedTag
	 * 		If true, the XML Element is tag-closed element
	 */
	@Override
	public void startElement(QName name, boolean closedTag) {
		name = this.validator.validateQName(name);
		JSONObject jo = this.jsonFactory.createObject();
		stack.push(jo);
	}

	/**
	 * Convert XML Attribute to JSON objects recursively.
	 * @param name
	 * 		QName of the Attribute
	 * @param value
	 * 		Value of the Attribute
	 */
	@Override
	public void attribute(QName name, String value) {
		name = this.validator.validateQName(name);
		value = (String) validator.validateCData(value);
		value = XmlChar.unescape(value);
		if (XmlNameNode.isNamespaceName(name)) {
			if (!ignoredNamespace) {
				nsAttribute(name, value);
			}
		} else {
			JSONObject parent = (JSONObject) stack.peek();
			String key = getAttributeKey(name);
			this.appendToParent(parent, key,
					this.jsonFactory.createValue(value));
		}
	}

	/**
	 * Convert name space Attribute to JSON objects.
	 * 
	 * @param name
	 * 		QName of the Attribute
	 * @param value
	 * 		Value of the Attribute
	 */
	protected void nsAttribute(QName name, String value) {
		name = validator.validateQName(name);
		value = (String) validator.validateCData(value);
		value = XmlChar.unescape(value);
		JSONObject parent = (JSONObject) stack.peek();
		String key = getAttributeKey(name);
		this.appendToParent(parent, key, this.jsonFactory.createValue(value));
	}

	/**
	 * Convert XML Text node to JSON objects.
	 * @param content
	 * 		Content of the Text node
	 */
	@Override
	public void text(String content) {
		content = (String) validator.validateCData(content);
		content = XmlChar.unescape(content);
		JSONObject parent = (JSONObject) stack.peek();
		this.appendToParent(parent, this.getTextKey(),
				this.jsonFactory.createValue(content));
	}

	/**
	 * Convert XML CDATA node to JSON objects.
	 * @param content
	 * 		Content of the CDATA node
	 */
	@Override
	public void cdata(String content) {
		content = (String) validator.validateCData(content);
		content = XmlChar.unescape(content);
		JSONObject parent = (JSONObject) stack.peek();
		this.appendToParent(parent, this.getCdatakey(),
				this.jsonFactory.createValue(content));
	}

	@Override
	public void comment(String content) {
		// ignored

	}

	/**
	 * Post handling for JSON objects generated by the XML Element.
	 * @param name
	 * 		The XML Element
	 * @param closedTag
	 * 		If true, the XML Element is tag-closed
	 */
	@Override
	public void endElement(QName name, boolean closedTag) {
		if (this.removeRoot) {
			if (stack.size() <= 2) {
				return;
			}
		}
		if (stack.size() <= 1) {
			return;
		}
		JSONObject jo = (JSONObject) stack.pop();
		JSONObject parent = (JSONObject) stack.peek();
		int size = jo.size();
		String key = getTagKey(name);
		if (size == 0) {
			if (keepEmptyElement) {
				appendToParent(parent, key, jo);
			} else {
				if (this.closedElement2null) {
					if (closedTag) {
						appendToParent(parent, key,
								this.jsonFactory.createValue(null));
					} else {
						appendToParent(parent, key,
								this.jsonFactory.createValue(""));
					}
				} else {
					appendToParent(parent, key,
							this.jsonFactory.createValue(null));
				}
			}
		} else if (size == 1) {
			Entry<String, JSONDocument> entry = jo.entryIterator().next();
			if (this.getTextKey().equals(entry.getKey())
					&& tagAskeyforSingleText) {
				appendToParent(parent, key, entry.getValue());
			} else {
				appendToParent(parent, key, jo);
			}
		} else {
			appendToParent(parent, key, jo);
		}
	}

	@Override
	public void endDocument() {

	}

	/**
	 * Append JSON Entry with key, Value to JSON Object.
	 * 
	 * @param parent
	 * 		The JSON Object
	 * @param key
	 * 		Key of the JSON Entry
	 * @param value
	 * 		Value of the JSON Entry
	 */
	protected void appendToParent(JSONObject parent, String key,
			JSONDocument value) {
		appendToParent(parent, key, value, true);
	}

	/**
	 * Append JSON Entry with key, Value to JSON Object.
	 * @param parent
	 * 		The JSON Object
	 * @param key
	 * 		Key of the JSON Entry
	 * @param value
	 * 		Value of the JSON Entry
	 * @param allowDuplicated
	 * 		If true, will allow duplicated JSON keys
	 */
	protected void appendToParent(JSONObject parent, String key,
			JSONDocument value, boolean allowDuplicated) {
		JSONDocument sibling = parent.getValue(key);
		if (sibling == null) {
			this.jsonFactory.objectEntry(parent, key, value);
		} else if (!allowDuplicated) {
			return;
		} else if (JSONArray.class.isInstance(sibling)) {
			JSONArray ja = (JSONArray) sibling;
			this.jsonFactory.arrayValue(ja, value);
		} else {
			JSONArray ja = this.jsonFactory.createArray();
			this.jsonFactory.arrayValue(ja, sibling, value);
			this.jsonFactory.objectEntry(parent, key, ja);
		}
	}

	/**
	 * @return the ignoredNamespace
	 */
	public boolean isIgnoredNamespace() {
		return ignoredNamespace;
	}

	/**
	 * @param ignoredNamespace
	 *            the ignoredNamespace to setHeader
	 */
	public void setIgnoredNamespace(boolean ignoredNamespace) {
		this.ignoredNamespace = ignoredNamespace;
	}

	/**
	 * @return the ignoredPrefix
	 */
	public boolean isIgnoredPrefix() {
		return ignoredPrefix;
	}

	/**
	 * @param ignoredPrefix
	 *            the ignoredPrefix to setHeader
	 */
	public void setIgnoredPrefix(boolean ignoredPrefix) {
		ignoredPrefix = ignoredPrefix && ignoredNamespace;
		this.ignoredPrefix = ignoredPrefix;
	}

	/**
	 * @return the tagAskeyforSingleText
	 */
	public boolean isTagAskeyforSingleText() {
		return tagAskeyforSingleText;
	}

	/**
	 * @param tagAskeyforSingleText
	 *            the tagAskeyforSingleText to setHeader
	 */
	public void setTagAskeyforSingleText(boolean tagAskeyforSingleText) {
		this.tagAskeyforSingleText = tagAskeyforSingleText;
	}

	/**
	 * @return the keepEmptyElement
	 */
	public boolean isKeepEmptyElement() {
		return keepEmptyElement;
	}

	/**
	 * @param keepEmptyElement
	 *            the keepEmptyElement to setHeader
	 */
	public void setKeepEmptyElement(boolean keepEmptyElement) {
		this.keepEmptyElement = keepEmptyElement;
	}

	/**
	 * Get JSON factory to create JSON objects
	 * @return The JSON factory
	 */
	public JSONFactory getJSONFactory() {
		return jsonFactory;
	}

	/**
	 * Set JSON key for Text node
	 * @param textKey
	 *            JSON key 
	 */
	public void setTextKey(String textKey) {
		this.textKey = textKey;
	}

	/**
	 * Get JSON key for Element tag by QName
	 * @param name
	 * 		The QName
	 * @return
	 * 		JSON key for Element tag
	 */
	public String getTagKey(QName name) {
		if (ignoredPrefix) {
			return name.getLocalPart();
		}
		if (StringUtils.isEmpty(name.getPrefix())) {
			return name.getLocalPart();
		}
		return name.getPrefix() + ":" + name.getLocalPart();
	}

	/**
	 * Get JSON key for Attribute by QName
	 * @param name
	 * 		The QName
	 * @return
	 * 		JSON key for Attribute
	 */
	public String getAttributeKey(QName name) {
		if (ignoredPrefix) {
			return name.getLocalPart();
		}
		return XmlNameNode.getName(name);
	}
	/**
	 * Get JSON key for name space by QName
	 * @param name
	 * 		The QName
	 * @return
	 * 		JSON key for name space 
	 */
	public String getNamespaceKey(QName name) {
		return getAttributeKey(name);
	}

	public String getTextKey() {
		return textKey;
	}

	public String getCdatakey() {
		return getTextKey();
	}

	/**
	 * Get Validator to validate XML string
	 * 
	 * @return The Validator
	 */
	public ValidatorChain getValidator() {
		return validator;
	}

	/**
	 * Get JSON Document generated after conversion.
	 * @return
	 * 		The JSON Document
	 */
	public JSONDocument getJSONDocument() {
		return stack.peek();
	}

	/**
	 * @return the removeRoot
	 */
	public boolean isRemoveRoot() {
		return removeRoot;
	}

	/**
	 * @param removeRoot
	 *            the removeRoot to setHeader
	 */
	public void setRemoveRoot(boolean removeRoot) {
		this.removeRoot = removeRoot;
	}

	/**
	 * @return the closedElement2null
	 */
	public boolean isClosedElement2null() {
		return closedElement2null;
	}

	/**
	 * @param closedElement2null
	 *            the closedElement2null to setHeader
	 */
	public void setClosedElement2null(boolean closedElement2null) {
		if (this.keepEmptyElement)
			return;
		this.closedElement2null = closedElement2null;
	}

}